#![feature(async_closure)]

extern crate hyper;

extern crate futures;
extern crate tokio;
use notify::op::Op;
use notify::{raw_watcher, RecursiveMode, Watcher};
use std::sync::mpsc::channel;

use std::convert::Infallible;

use anyhow::Error;

use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use walkdir::WalkDir;

use std::sync::Arc;
use tokio::sync::{RwLock, RwLockReadGuard};

extern crate slog;
extern crate slog_async;
extern crate slog_json;
use slog::Drain;
use slog::Level;
use slog_scope::{debug, error, info};

use std::fs::OpenOptions;
use std::path::{Path, PathBuf};

use std::collections::HashMap;
use std::fs::File;
use std::io::Read;

extern crate clap;
use clap::{App, Arg};

extern crate regex;
use regex::Regex;

// https://gist.github.com/rust-play/affe772f93dd8b6a008b004adc4fd19e
fn setup_logger(log_path: &str, level: Level) -> slog_scope::GlobalLoggerGuard {
    let file = OpenOptions::new()
        .create(true)
        .truncate(false)
        .append(true)
        .open(log_path)
        .unwrap();

    let drain = slog_json::Json::new(file).add_default_keys().build().fuse();

    let drain = slog_async::Async::new(drain).build().fuse();
    let drain = slog::LevelFilter(drain, level).fuse();
    let logger = slog::Logger::root(drain, slog::slog_o!());

    slog_scope::set_global_logger(logger)
}

pub struct FileServer {
    base_path: PathBuf,
    exclude_path_regex: Option<Regex>,
    file_contents: HashMap<String, Vec<u8>>,
}

impl<'a> FileServer {
    pub fn new(base_path: &Path, exclude_path_regex: Option<Regex>) -> FileServer {
        FileServer {
            base_path: base_path.to_owned(),
            exclude_path_regex,
            file_contents: HashMap::new(),
        }
    }

    fn serve(&self, p: &'a str) -> Response<Body> {
        match self.file_contents.get(p) {
            Some(contents) => {
                let body = Body::from(contents.clone());
                Response::new(body)
            }
            _ => not_found(),
        }
    }

    pub fn remove_file(&mut self, file_abs_path: &Path) -> Result<(), Error> {
        let key = "/".to_string()
            + &file_abs_path
                .strip_prefix(&self.base_path)?
                .display()
                .to_string();

        info!(
            "{}",
            format!("will stop serving {} as {}", file_abs_path.display(), key)
        );

        self.file_contents.remove(&key);
        Ok(())
    }

    pub fn reload_file(&mut self, file_abs_path: &Path) -> Result<(), Error> {
        if let Some(regex) = &self.exclude_path_regex {
            if file_abs_path
                .to_str()
                .map(|s| regex.is_match(s))
                .unwrap_or(false)
            {
                return Err(anyhow::Error::msg(format!(
                    "reload: ignoring file {:?}",
                    file_abs_path
                )));
            }
        }

        let key = "/".to_string()
            + &file_abs_path
                .strip_prefix(&self.base_path)?
                .display()
                .to_string();

        let mut file = File::open(file_abs_path)?;
        let mut s: Vec<u8> = Vec::with_capacity(file.metadata().unwrap().len() as usize);
        file.read_to_end(&mut s).unwrap();

        info!(
            "{}",
            format!("will serve {} as {}", file_abs_path.display(), key)
        );
        self.file_contents.insert(key, s);
        Ok(())
    }

    pub fn initialize_dir(&mut self) -> Result<&mut Self, Error> {
        let regex = self.exclude_path_regex.clone();
        let is_match_ok = |s: &str| match &regex {
            Some(ex) => !ex.is_match(&s),
            None => true,
        };

        for entry in WalkDir::new(&self.base_path)
            .follow_links(false)
            .into_iter()
            // Do not consider files whose pathname is excluded by -e:
            .filter_map(|e| e.ok())
            .filter(|e| e.path().to_str().map(&is_match_ok).unwrap_or(false))
        {
            if entry.file_type().is_file() {
                info!("{:?}", &entry.path().display());
                self.reload_file(&entry.path().canonicalize()?)?;
            }
        }
        Ok(self)
    }
}

fn not_found() -> Response<Body> {
    let mut res = Response::new(Body::from("404 resource not found."));
    *res.status_mut() = StatusCode::NOT_FOUND;
    res
}

fn serve_it(
    file_server: RwLockReadGuard<FileServer>,
    req: Request<Body>,
) -> Result<Response<Body>, Infallible> {
    match *req.method() {
        Method::GET => {
            info!("{:?}", req);

            let req_path = Path::new(req.uri().path());

            if !req_path.has_root() {
                return Ok(not_found());
            }

            info!("Got request for {:?}", req_path.to_str());

            match req_path.to_str() {
                Some(str) => {
                    if str.ends_with('/') {
                        Ok(file_server.serve(&(str.to_string() + "index.html")))
                    } else {
                        Ok(file_server.serve(str))
                    }
                }
                _ => Ok(not_found())
            }
        }
        _ => Ok(not_found())
    }
}

#[tokio::main(core_threads=4)]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let matches = App::new("minimal_web")
        .version("0.1.1")
        .author("fnordomat <GPG:46D46D1246803312401472B5A7427E237B7908CA>")
        .about("A simple static-web server")
        .arg(
            Arg::with_name("base")
                .short("b")
                .long("base")
                .takes_value(true)
                .help("Base directory (serve this directory)"),
        )
        .arg(
            Arg::with_name("exclude_path")
                .short("e")
                .long("exclude-path")
                .takes_value(true)
                .multiple(true)
                .help("Exclude part of path (glob) (do not serve these files)"),
        )
        .get_matches();

    let mydir = matches.value_of("base").unwrap_or("files").to_owned();
    let mut exclude_exprs: Vec<&str> = matches
        .values_of("exclude_path")
        .map_or([].to_vec(), |x| x.collect());

    use std::env;
    let cwd = env::current_dir()?.canonicalize()?;
    println!("The current directory is {:?}", cwd);

    // Always avoid serving dotfiles, which typically aren't intended to be public
    exclude_exprs.push("(^|[/])[.].*");
    let exclude_exprs = exclude_exprs;

    let maybe_exclude_path_regex = if exclude_exprs.is_empty() {
        None
    } else {
        Some(Regex::new(&exclude_exprs.join("|")).unwrap())
    };

    let log_path = "server.log";
    // let _guard = setup_logger(log_path, slog::Level::Info);
    // let _guard = setup_logger(log_path, slog::Level::Debug);
    let _guard = setup_logger(log_path, slog::Level::Error);

    let mut file_server =
        FileServer::new(&Path::new(&mydir).canonicalize()?, maybe_exclude_path_regex);
    file_server.initialize_dir()?;

    let file_server = Arc::new(RwLock::new(file_server));
    let file_server_clone = file_server.clone();

    let make_svc = make_service_fn(move |_socket: &AddrStream| {
        let file_server = file_server.clone();
        async move {
            let file_server = file_server.clone();
            Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
                let file_server = file_server.clone();
                async move { serve_it(file_server.read().await, req) }
            }))
        }
    });

    // Listen on private interface only!
    let addr = ([127, 0, 0, 1], 8080).into();
    let server = Server::bind(&addr).serve(make_svc);

    tokio::spawn({
        async move {
            info!("initializing file notify watcher");

            let (tx, rx) = channel();
            let mut watcher = raw_watcher(tx).unwrap();
            watcher.watch(mydir, RecursiveMode::Recursive).unwrap();

            loop {
                let recvd = rx.recv();
                match recvd {
                    Ok(event) => {
                        debug!("{:?}", event);
                        if let Err(e) = match (event.op, event.path) {
                            (Ok(Op::CLOSE_WRITE), Some(path)) => {
                                file_server_clone.write().await.reload_file(&path)
                            }
                            (Ok(Op::CREATE), Some(path)) => {
                                file_server_clone.write().await.reload_file(&path)
                            }
                            (Ok(Op::REMOVE), Some(path)) => {
                                file_server_clone.write().await.remove_file(&path)
                            }
                            // Simply let the first RENAME event fail instead of handling the cookie.
                            (Ok(Op::RENAME), Some(path)) => {
                                file_server_clone.write().await.reload_file(&path)
                            }
                            (Err(e), _) => {
                                error!("watch error: {:?}", e);
                                Err(anyhow::Error::from(e))
                            }
                            _ => {
                                // uninteresting
                                Ok(())
                            }
                        } {
                            error!("watch event caused fileserver error: {:?}", e);
                        }
                    }
                    Err(e) => error!("watch event receive error: {:?}", e),
                }
            }
        }
    });

    println!("Listening on http://{}", addr);
    info!("{}", format!("Listening on http://{}", addr));

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    };

    Ok(())
}
